#
# @@-COPYRIGHT-START-@@
#
# Copyright (c) 2014-2015 Qualcomm Atheros, Inc.
# All rights reserved.
# Qualcomm Atheros Confidential and Proprietary.
#
# @@-COPYRIGHT-END-@@
#

"""Facilities for receiving diagnostic logging data over the network.

The following classes are exported:

    :class:`NetworkReaderRaw`: synchronous network reads with a callback
        that provides the raw byte data
    :class:`NetworkReaderParsed`: synchronous network reads with a
        callback that provides the parsed data
"""

import socket
import errno
import threading
import logging

log = logging.getLogger('whcdiagnw')


class NetworkReaderRaw(object):
    """Simple reader of diagnostic logging data over UDP.

    This reader will continuously read from the socket and provide a
    callback for each packet received. It does not do any parsing of
    the data.
    """

    """Port on which diagnostic logs are sent by default."""
    DEFAULT_PORT = 7788

    """Port on which hyd diagnostic logs are sent by default."""
    DEFAULT_HYD_PORT = 5555

    """Maximum number of bytes allowed in a single message.
    This is the maximum size generated by logService.c
    """
    MAX_MSG_SIZE = 8192

    class MsgHandler(object):
        """Interface for processing received messages.

        Derived classes override :method:`process_msg` to plug in their
        desired behavior.
        """

        def __init__(self):
            pass

        def process_msg(self, src_addr, data):
            """Process a single received diagnostic logging message.

            Args:
                src_addr (str): the address of the node that sent the log
                data (str): the raw bytes received
            """
            pass

    def __init__(self, ip_addr='', port=DEFAULT_PORT):
        """Initialize the socket to listen on the specified port.

        Args:
            ip_addr (str): the address on which to bind
            port (int): the port on which to listen
        """
        self._ip_addr = ip_addr
        self._port = port
        self._shutdown_requested = threading.Event()

    def receive_msgs(self, handler):
        """Continuously receive messages in a loop.

        Each message received is provided in its raw form (eg. as a
        packed string) to the handler.

        Note: This function will continue receiving messages until
        close() is called.

        Args:
            handler (:class:`MsgHandler`): the handler object to pass
                the received messages to
        """
        while not self._shutdown_requested.is_set():
            try:
                (data, addr) = self._socket.recvfrom(self.MAX_MSG_SIZE)
            except socket.timeout:
                continue
            except socket.error as err:
                # We allow for an interrupted receive so that a signal
                # can be used to shut down the loop.
                if err.errno != errno.EINTR:
                    raise
                else:
                    continue

            handler.process_msg(addr, data)

        self._shutdown_requested.clear()

    def terminate_receive(self):
        """Terminate the :method:`receive_msgs` loop.

        This only signals that the loop should terminate. The caller
        cannot assume it has terminated upon this function's return.
        """
        self._shutdown_requested.set()

    def __enter__(self):
        """Initialize the socket on the desired IP and port."""
        self._socket = socket.socket(type=socket.SOCK_DGRAM)
        self._socket.settimeout(0.1)

        log.info("Binding to %s:%u", self._ip_addr, self._port)
        self._socket.bind((self._ip_addr, self._port))

        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Close the socket.

        It is expected that :method:`receive_msgs` is no longer active
        when the enclosing scope is exited.
        """
        self._socket.close()
